﻿namespace Comet.Infrastructure
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using EnvDTE;

    /// <summary>   
    ///     Comet utils. A collection of useful methods 
    ///     that can be used for code analysis.
    /// </summary>
    /// <remarks>   
    ///     Simon, 10.07.2010. 
    /// </remarks>
    public static class CometUtils
    {
        /// <summary>   
        ///     Generates the constructors from superclass. 
        /// </summary>
        /// <remarks>   
        ///     Simon, 10.07.2010. 
        /// </remarks>
        /// <param name="derivedClass"> The derived class. </param>
        public static void GenerateConstructorsFromSuperclass(CodeClass derivedClass)
        {
            //Search for available constructors in passed class
            IEnumerable<CodeFunction> constructorsDerivedClass = derivedClass.Members.OfType<CodeFunction>()
                                                                                     .Where(x => x.FunctionKind == vsCMFunction.vsCMFunctionConstructor);

            CodeFunction lastConstructor = constructorsDerivedClass.LastOrDefault();

            //go through base-class(es)
            foreach (CodeClass baseClass in derivedClass.Bases.OfType<CodeClass>())
            {
                //all constructors of base class
                IEnumerable<CodeFunction> constructorsBaseClass = baseClass.Members.OfType<CodeFunction>()
                                                                                   .Where(x => x.FunctionKind == vsCMFunction.vsCMFunctionConstructor);

                //iterate through all constructors. Implement those the derived class does not have
                foreach (CodeFunction baseConstructor in constructorsBaseClass)
                {
                    bool hasConstructor = HasConstructor(derivedClass, baseConstructor);

                    if (!hasConstructor)
                    {
                        //create new constructor for derived class
                        CodeFunction constructorCodeClass = derivedClass.AddFunction(
                            derivedClass.Name,
                            vsCMFunction.vsCMFunctionConstructor,
                            vsCMTypeRef.vsCMTypeRefVoid,
                            lastConstructor ?? (object)0,
                            vsCMAccess.vsCMAccessPublic, null);


                        //create parameter list
                        int i = 0;

                        StringBuilder parameters = new StringBuilder();
                        foreach (CodeParameter param in baseConstructor.Parameters)
                        {
                            constructorCodeClass.AddParameter(param.Name, param.Type, i);
                            parameters.Append(param.Name);

                            if (i < baseConstructor.Parameters.Count - 1)
                                parameters.Append(", ");

                            i++;
                        }

                        //TODO: this works only for C#
                        //append to end of line
                        EditPoint editPoint = constructorCodeClass.StartPoint.CreateEditPoint();
                        editPoint.MoveToLineAndOffset(editPoint.Line, editPoint.LineLength + 1);
                        editPoint.Insert(" : base(" + parameters + ")");

                        //newly created constructor is now last constructor
                        lastConstructor = constructorCodeClass;
                    }
                }
            }
        }



        /// <summary>   
        ///     Generates a constructor from fields. 
        /// </summary>
        /// <remarks>   
        ///     Simon, 10.07.2010. 
        /// </remarks>
        /// <param name="codeClass">    The code class. </param>
        public static void GenerateConstructorFromDataMembers(CodeClass codeClass, IEnumerable<CodeElement> dataMembers, int indentationSize)
        {
            CodeFunction lastConstructor = codeClass.Members.OfType<CodeFunction>()
                                                            .Where(x => x.FunctionKind == vsCMFunction.vsCMFunctionConstructor)
                                                            .LastOrDefault();

            CodeFunction constructor = codeClass.AddFunction(
                codeClass.Name,
                vsCMFunction.vsCMFunctionConstructor,
                vsCMTypeRef.vsCMTypeRefVoid,
                lastConstructor ?? (object)0,
                vsCMAccess.vsCMAccessPublic,
                null);

            int i = 0;
            foreach (var dataMember in dataMembers)
            {
                string parameterName = null;
                CodeTypeRef parameterType = null;

                if (dataMember is CodeVariable)
                {
                    CodeVariable field = dataMember as CodeVariable;
                    parameterName = BeautifyParameterName(field.Name);
                    parameterType = field.Type;
                }

                if (dataMember is CodeProperty)
                {
                    CodeProperty property = dataMember as CodeProperty;
                    parameterName = BeautifyParameterName(property.Name);
                    parameterType = property.Type;
                }

                constructor.AddParameter(parameterName, parameterType, i);

                i++;
            }

            EditPoint editPoint = constructor.GetStartPoint().CreateEditPoint();

            string text = editPoint.GetText(constructor.GetEndPoint());
            int index = text.IndexOf('{');

            editPoint.MoveToAbsoluteOffset(editPoint.AbsoluteCharOffset + index);
            int indentation = editPoint.LineCharOffset + indentationSize;

            editPoint.MoveToAbsoluteOffset(editPoint.AbsoluteCharOffset + 1);

            constructor.Parameters.GetEnumerator().Reset();
            var parameters = constructor.Parameters.OfType<CodeParameter>();
            for (i = 0; i < dataMembers.Count(); i++)
            {
                CodeParameter parameter = parameters.ElementAt(i);
                CodeElement dataMember = dataMembers.ElementAt(i);
                editPoint.PadToColumn(indentation);

                string insertionText = "this." + dataMember.Name + " = " + parameter.Name + ";";
                editPoint.Insert(insertionText + Environment.NewLine);
            }

        }

        /// <summary>   
        ///     Generates ToString() for the passed data members. 
        /// </summary>
        /// <remarks>   
        ///     Simon, 18.02.2011. 
        /// </remarks>
        /// <param name="codeClass">    The class where the toString() will be generated. </param>
        /// <param name="dataMembers">  The data members represented by a string. </param>
        public static void GenerateToStringFromDataMembers(CodeClass codeClass, IEnumerable<CodeElement> dataMembers, int indentationSize)
        {
            CodeFunction toStringMethod = codeClass.Members.OfType<CodeFunction>().FirstOrDefault(x => x.Name == "ToString");
            toStringMethod = toStringMethod ?? codeClass.AddFunction("ToString",
                vsCMFunction.vsCMFunctionFunction,
                vsCMTypeRef.vsCMTypeRefString,
                null,
                vsCMAccess.vsCMAccessPublic, null);

            EditPoint editPoint = toStringMethod.GetStartPoint().CreateEditPoint();

            //create the method body
            string toStringBody = "string result = \"\";";
            string endOfStatement = ";" + Environment.NewLine;
            string toStringTemplate = "\"[{0}] = \"+{0}.ToString() + \"; \"";
            string nullTemplate = "\"[{0}] = null; \"";
            string ifElseNullTemplateLong = "if({0} != null) { {1} } else { {2} }";
            string ifElseNullTemplateShort = "{0} != null? {1} : {2}";

            foreach (CodeElement codeElement in dataMembers)
            {
                dynamic dataMember = codeElement;
                CodeProperty codeProperty = codeElement as CodeProperty;
                CodeVariable codeVariable = codeElement as CodeVariable;

                vsCMTypeRef typeKind = (vsCMTypeRef)dataMember.Type.TypeKind;

                bool isObject = typeKind == vsCMTypeRef.vsCMTypeRefObject
                    || typeKind == vsCMTypeRef.vsCMTypeRefString;

                CodeType codeType = (CodeType)dataMember.Type.CodeType;
                if (codeType != null)
                {
                    isObject = isObject || codeType.Kind == vsCMElement.vsCMElementClass;
                }

                if (isObject)
                {
                    string ifBody = string.Format(toStringTemplate, codeElement.Name);
                    string elseBody = string.Format(nullTemplate, codeElement.Name);
                    string ifElseStatement = string.Format(ifElseNullTemplateShort, codeElement.Name, ifBody, elseBody);
                    toStringBody += "result+=" + ifElseStatement + endOfStatement;
                }
                else
                {
                    toStringBody += "result+=";
                    toStringBody += string.Format(toStringTemplate, codeElement.Name) + endOfStatement;
                }
            }

            toStringBody += "return result";

            //text replacement and formatting
            editPoint.ReplacePattern(toStringMethod.EndPoint, "return default(string)", toStringBody);
            editPoint.SmartFormat(toStringMethod.EndPoint);
        }
        /// <summary>   
        ///     Beautify parameter name. 
        /// </summary>
        /// <remarks>   
        ///     Simon, 13.02.2011. 
        /// </remarks>
        /// <param name="name"> The name. </param>
        /// <returns>   
        ///     . 
        /// </returns>
        public static string BeautifyParameterName(string name)
        {
            string newName = name.Replace("m_", "");
            string newNameTemp = newName.Substring(0, 1).ToLower();

            if (newName.Length > 1)
            {
                newNameTemp += newName.Substring(1, newName.Length - 1);
            }

            newName = newNameTemp;

            return newName;
        }

        /// <summary>   
        ///     Checks if the class represented by codeClass has the passed
        ///     constructor.
        /// </summary>
        /// <remarks>   
        ///     Simon, 10.07.2010. 
        /// </remarks>
        /// <param name="codeClass">    The code class. </param>
        /// <param name="constructor">  The constructor. </param>
        /// <returns>   
        ///     true if constructor was found, false if not. 
        /// </returns>
        public static bool HasConstructor(CodeClass codeClass, CodeFunction constructor)
        {
            bool hasConstructor = false;
            //same number of parameters
            var constructors = codeClass.Members.OfType<CodeFunction>().Where(x => x.FunctionKind == vsCMFunction.vsCMFunctionConstructor);

            foreach (var constructorToCheck in constructors)
            {
                if (constructor.Parameters.Count == constructorToCheck.Parameters.Count)
                {
                    hasConstructor = true;

                    var constructorEnumerator = constructor.Parameters.GetEnumerator();
                    var constructorToCheckEnumerator = constructorToCheck.Parameters.GetEnumerator();

                    for (int i = 0; i < constructor.Parameters.Count; i++)
                    {
                        constructorEnumerator.MoveNext();
                        constructorToCheckEnumerator.MoveNext();

                        CodeParameter baseParameter = constructorEnumerator.Current as CodeParameter;
                        CodeParameter derivedParameter = constructorToCheckEnumerator.Current as CodeParameter;

                        if (baseParameter.Type.AsFullName != derivedParameter.Type.AsFullName)
                        {
                            hasConstructor = false;

                            //reset enumerators
                            constructorEnumerator.Reset();
                            constructorToCheckEnumerator.Reset();

                            break;
                        }
                    }

                    //reset enumerators
                    constructorEnumerator.Reset();
                    constructorToCheckEnumerator.Reset();
                }
            }

            return hasConstructor;
        }

        /// <summary>   
        ///     Converts the vsCMAccess to a DataMemberVisibility enumeration.
        /// </summary>
        /// <remarks>   
        ///     Simon, 18.02.2011. 
        /// </remarks>
        /// <exception cref="ArgumentException">    Thrown when vsCMAccess has an illegal value. </exception>
        /// <param name="vsCMAccess">   The value to convert. </param>
        /// <returns>   
        ///     This passed value as a DataMemberVisibility. 
        /// </returns>
        public static DataMemberVisibility ToDataMemberVisibility(vsCMAccess vsCMAccess)
        {
            switch (vsCMAccess)
            {
                case EnvDTE.vsCMAccess.vsCMAccessPublic:
                    return DataMemberVisibility.PublicAccess;
                case EnvDTE.vsCMAccess.vsCMAccessProtected:
                    return DataMemberVisibility.ProtectedAccess;
                case EnvDTE.vsCMAccess.vsCMAccessProject:
                    return DataMemberVisibility.InternalAccess;
                case EnvDTE.vsCMAccess.vsCMAccessPrivate:
                    return DataMemberVisibility.PrivateAccess;
                default:
                    throw new ArgumentException("This type of access is not supported: " + vsCMAccess.ToString());
            }
        }

        /// <summary>   
        ///     Extracts the settable non static data members from the passed codeClass and
        ///     returns them in a ExtractedDataMembers object.
        /// </summary>
        /// <remarks>   
        ///     Simon, 18.02.2011. 
        /// </remarks>
        /// <param name="codeClass">    The code class where to extract data members from. </param>
        /// <returns>   
        ///     An object containing the extracted data members. 
        /// </returns>
        public static ExtractedDataMembers ExtractSettableNonStaticDataMembers(CodeClass codeClass)
        {
            //non-static fields
            var fields = codeClass.Members.OfType<CodeVariable>().Where(x => !x.IsShared);

            //non-static settable properties
            var properties = codeClass.Members.OfType<CodeProperty>().Where(x => x.Setter != null && !x.Setter.IsShared);

            return new ExtractedDataMembers(properties, fields);
        }
    }
}